<?php
# Set time limit to indefinite execution and show errors
set_time_limit(0);
error_reporting(E_ERROR | E_WARNING | E_PARSE);

#Create the clients array
$clients = array();
#Socket array
$sockets = array();

$lastFlush = time();

#Server info
$host = '192.168.1.100';
$port = 14100;

# Create a TCP Stream socket
$master = socket_create(AF_INET, SOCK_STREAM, 0) or die("Can't create the socket.\n");
socket_bind($master, $host, $port) or die("Could not bind to socket $host:$port.\n");
socket_listen($master) or die("Could not set up socket listener\n");
echo "Listening on $host:$port\n";

#Loop infinitely (Runs on petition)
while(true){
	#Add clients socket to the sockets array.
	foreach($clients as $client){
		$sockets[$client->uid] = $client->socket;
	}
	
	$now = time();
	echo '----- '.date('H:i:s',$now).' - Memory: '.round(memory_get_usage()/1024,2).'Kb - Peak: '.round(memory_get_peak_usage()/1024,2)."Kb\n";
	#Kill dead clients every 30 seconds
	if($now > $lastFlush+30){
		$lastFlush = $now;
		dropDeadClients();
		showClients();
	}
	
	#Set up a blocking call to socket_select()
	$sockets[0] = $master;
	$ready = socket_select($sockets,$w,$e,10);
	if($ready === 0){
		#Timeout - do start over
		continue;
	}
	
	#Add the connection to the clients array and star over listening for more
	if(in_array($master,$sockets)){
		new client(socket_accept($master));
		showClients();
		continue;
	}
	
	#Proceed to read the writted msg
	foreach($clients as $key => $client){
		$rawData = str_replace('}{','}|{',socket_read($client->socket,1024));
		$rawData = explode('|',$rawData);
		
		foreach($rawData as $rawDataChunk){
			#Decode the info
			$data = json_decode($rawDataChunk);
			
			#Bad request, show error and ignore it.
			if($data == NULL){
				echo 'Invalid request from '.$client->username.' at '.$client->ip.':'.$client->port.' - '.$rawDataChunk."\n";
				if(empty($rawDataChunk)){
					echo "|--Possible bot, dropping the jackass.\n";
					socket_close($sockets[$key+1]);
					unset($sockets[$key+1]);
					unset($clients[$key]);
				}
				continue;
			#Chat send
			}elseif($data->type == 'chat'){
				echo $client->username.' said: '.$data->msg."\n";
				broadcast('{"type":"chat","msg":"'.$data->msg.'","username":"'.$client->username.'"}');
			#System message (New connection, Login, Logout, etc)
			}elseif($data->type == 'system'){
				#New connection (Client open)
				if($data->msg == 'newConnection'){
					echo 'Client opened at '.$client->ip.':'.$client->port."\n";
				#Client login
				}elseif($data->msg == 'loginSuccess'){
					$client->username = $data->username;
					$client->userId = $data->userId;
					echo $client->username.'('.$client->userId.') logged in from '.$client->ip.':'.$client->port."\n";
					broadcast('{"type":"server","msg":"'.$data->username.' logged in."}');
					broadcast('{"type":"system","msg":"playerJoin","userId":'.$data->userId.'}');
				}elseif($data->msg == 'ping'){
					$client->tell('{"type":"system","msg":"pong"}');
				}
			#Move related
			}elseif($data->type == 'move'){
				echo $client->username.' moved '.$data->direction.' starting {at x:'.$data->from->x.', y:'.$data->from->y.', z:'.$data->from->z."}\n";
				broadcast('{"type":"move","direction":"'.$data->direction.'","userId":'.$client->userId.',"from":{"x":'.$data->from->x.',"y":'.$data->from->y.',"z":'.$data->from->z.'}}');
			}else{
				echo "Didn't handled a well formated JSON: $rawData\n";
			}
		}
	}
}

#############################
### CLASSES AND FUNCTIONS ###
#############################

function broadcast($msg){
	global $clients;
	
	foreach($clients as $client){
		$client->tell($msg);
	}
}

function showClients(){
	global $clients;
	
	foreach($clients as $key => $client){
		echo 'Client '.$key.' (UID: '.$client->uid.') is '.$client->username.' ('.$client->userId.') from '.$client->ip.':'.$client->port.' and socket '.$client->socket."\n";
	}
}

function dropDeadClients(){
	global $clients, $sockets;
	
	echo "|-Flushing zombies...\n";
	#Send a keepAlive, if player isn't there, kill it.
	foreach($clients as $client){
		$client->killIfDead();
	}
}

/**
* Client class!
*/
class client{
	var $uid, $ip, $port, $socket, $ping;
	var $userId, $username;
	
	function __construct($socket){
		global $clients, $sockets;
		
		#Socket
		$this->socket = $socket;
		
		#Add it to the clients and sockets array, then assign it an ID.
		array_push($clients,$this);
		array_push($sockets,$this->socket);
		$this->setId();
		#Figure out the clients ip
		socket_getpeername($this->socket,$this->ip,$this->port);
	}
	
	function setId(){
		global $clients;
		
		foreach($clients as $key => $client){
			if($client == $this){
				$this->uid = $key+1;
			}
		}
	}
	
	/**
	* Sends a message to this client.
	*/
	function tell($msg){
		socket_write($this->socket,$msg);
	}
	
	/**
	* Removes dead clients, also adds the client socket to the
	* socket array so we can listen to it (If the connection
	* still alive)
	*/
	function killIfDead(){
		global $clients, $sockets;
		
		#Is the client connection alive?
		$alive = @socket_write($this->socket,'{"type":"system","msg":"keepAlive"}');
		if($alive === FALSE){
			#Client disconnected
			$msg = '{"type":"system","msg":"'.$this->username.' ('.$this->ip.':'.$this->port.') dropped dead"}';
			echo '|--'.$this->username."'s connection droped dead\n";
			socket_close($sockets[$this->uid]);
			unset($sockets[$this->uid]);
			unset($clients[$this->uid-1]);
			return 1;
		}else{
			#Add the socket to the sockets list
			$msg = '{"type":"system","msg":"'.$this->ip.':'.$this->port.' still alive"}';
			echo '|--'.$this->username.' connection still alive '.$this->socket."\n";
			$sockets[$this->uid] = $this->socket;
			return 0;
		}
	}
}

?>